﻿using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;

namespace SpriteAnimator.Controls
{
    /// <summary>
    /// Contrôle custom pour l'afficahge du story board
    /// </summary>
    [Browsable(true), DefaultEvent("SelectedIndexChange")]
    public class usrAnimation : System.Windows.Forms.PictureBox
    {
        #region Evénements

        public event EventHandler<ControlPageArgs> ControlPageChange;
        public event EventHandler Modified;
        public event EventHandler SelectedIndexChange;

        #endregion

        #region Variables d'instances

        private int m_iRefCadre;
        private int m_iSelectedIndex;
        private int m_iDrawStartIndex;
        private int m_iDrawMaxPage;
        private bool m_bRangeMode;
        private bool m_bReDraw;
        private bool m_bToSave;
        private Font m_oFont;
        private Pen m_oPen;
        private Pen m_oPenRect;
        private SolidBrush m_oBrush;
        private List<int> m_oAnimation;

        #endregion

        #region Constructeur et destructeur

        /// <summary>
        /// Constructeur du contrôle
        /// </summary>
        public usrAnimation()
            : base()
        {
            // *** Style par défaut ***

            this.BorderStyle = BorderStyle.FixedSingle;
            this.BackColor = Color.CornflowerBlue;

            // *** Initialisation ***

            this.m_oAnimation = new List<int>();

            this.m_bToSave = false;
            this.m_bRangeMode = false;
            this.m_bReDraw = true;
            this.Frames = new List<Rectangle>();
            this.SelectedIndex = -1;
            this.m_iDrawStartIndex = 0;
            this.Texture = null;

            this.m_oBrush = new SolidBrush(Color.Black);
            this.m_oFont = new Font("Arial", 6.0f, FontStyle.Bold);
            this.m_oPenRect = new Pen(Color.Red, 2.0f);

            this.m_oPen = new Pen(Color.Black, 5.0f);
            this.m_oPen.StartCap = LineCap.NoAnchor;
            this.m_oPen.EndCap = LineCap.ArrowAnchor;

            // *** Calcul de la référence de la taille du cadre frame ***

            this.m_iRefCadre = this.Height - 18;
            this.m_iDrawMaxPage = this.Width / (this.m_iRefCadre + 4);

            // *** Surcharge événements ***

            this.Paint += new PaintEventHandler(this.ControlPaint);
            this.SizeChanged += new EventHandler(this.ControlSizeChanged);
            this.MouseClick += new MouseEventHandler(this.ControlMouseClick);
        }

        /// <summary>
        /// Libère les ressources
        /// </summary>
        /// <param name="disposing">TRUE dispose des objets managé, FALSE sinon</param>
        protected override void Dispose(bool disposing)
        {
            if ( disposing )
            {
                if ( this.m_oBrush != null )
                {
                    this.m_oBrush.Dispose();
                    this.m_oBrush = null;
                }

                if ( this.m_oFont != null )
                {
                    this.m_oFont.Dispose();
                    this.m_oFont = null;
                }

                if ( this.m_oPen != null )
                {
                    this.m_oPen.Dispose();
                    this.m_oPen = null;
                }

                if ( this.m_oPenRect != null )
                {
                    this.m_oPenRect.Dispose();
                    this.m_oPenRect = null;
                }
            }

            base.Dispose(disposing);
        }

        #endregion

        #region Evénéments du contrôle

        /// <summary>
        /// Se produit lors de la sélection d'un élément
        /// </summary>
        /// <param name="sender">Source de l'appel</param>
        /// <param name="e">Argument de l'appel</param>
        private void ControlMouseClick(object sender, MouseEventArgs e)
        {
            int iIndex = this.GetIndexOfSelection(e.X);

            if ( iIndex > -1 )
            {
                this.m_iSelectedIndex = iIndex;
                this.CheckAndRefresh();

                // *** Vérification changement d'index ***

                if ( this.m_iDrawStartIndex != iIndex && this.SelectedIndexChange != null )
                    this.SelectedIndexChange(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// On dessine de façon custum le contrôle
        /// </summary>
        /// <param name="sender">Source de l'appel</param>
        /// <param name="e">Argument de l'appel</param>
        private void ControlPaint(object sender, PaintEventArgs e)
        {
            // *** On efface tout ***

            e.Graphics.Clear(this.BackColor);

            // *** Vérification texture ***

            if ( this.Texture == null )
                return;

            // *** Routage dessin ***

            if ( this.m_bRangeMode )
                this.DrawRange(e.Graphics);
            else
                this.DrawList(e.Graphics);

            // *** On dessine le rectangle ***

            if ( this.m_iSelectedIndex > -1 )
            {
                int iPos = this.m_iSelectedIndex;

                if ( this.m_bRangeMode && iPos == 1 )
                    iPos++;

                e.Graphics.DrawRectangle(this.m_oPenRect, (iPos * (this.m_iRefCadre + 4)) + 1, 0, this.m_iRefCadre + 4, this.m_iRefCadre + 2);
            }
        }

        /// <summary>
        /// On change la taille des cadres de référence quand on retaille
        /// </summary>
        /// <param name="sender">Source de l'appel</param>
        /// <param name="e">Argument de l'appel</param>
        private void ControlSizeChanged(object sender, EventArgs e)
        {
            this.m_iRefCadre = this.Height - 18;
            this.m_iDrawMaxPage = this.Width / (this.m_iRefCadre + 4);

            if ( this.m_bReDraw )
                this.Invalidate();
        }

        #endregion

        #region Fonctions publiques

        /// <summary>
        /// Ajoute une frame à l'animation
        /// </summary>
        /// <param name="p_iFrameIndex">Index de la frame dans la liste</param>
        public void Add(int p_iFrameIndex)
        {
            // *** Vérification ***

            if ( p_iFrameIndex < 0 || p_iFrameIndex >= this.Frames.Count )
                return;
            else if ( this.m_bRangeMode && this.m_oAnimation.Count == 2 )
                return;

            // *** Ajout de la frame ***

            this.m_oAnimation.Add(p_iFrameIndex);

            // *** On se place à la fin si il le faut **

            if ( this.m_oAnimation.Count > this.m_iDrawMaxPage )
                this.m_iDrawStartIndex = this.m_oAnimation.Count - this.m_iDrawMaxPage;
            else if ( this.m_iDrawStartIndex < 0 )
                this.m_iDrawStartIndex = 0;

            // *** On rafraichis tous les statuts ***

            this.CheckAndRefresh();

            // *** Modifié ***

            this.IsModified();
        }

        /// <summary>
        /// Supprime le story board en cours
        /// </summary>
        public void Clear()
        {
            this.m_oAnimation.Clear();
            this.CheckAndRefresh();
        }

        /// <summary>
        /// Vérifie et informe sur la dispo des boutons de déplacement de page
        /// </summary>
        public void CheckPage()
        {
            bool bReturn = false;
            bool bFoward = false;

            // *** Vérification des possibilités ***

            if ( this.m_iDrawStartIndex > 0 )
                bReturn = true;

            if ( (this.m_iDrawStartIndex + this.m_iDrawMaxPage) < this.m_oAnimation.Count )
                bFoward = true;

            // *** Envoi de l'information page ***

            if ( this.ControlPageChange != null )
                this.ControlPageChange(this, new ControlPageArgs(bFoward, bReturn));
        }

        /// <summary>
        /// Fait défilé sur la frame suivante
        /// </summary>
        public void FrameNext()
        {
            int iOld = this.m_iDrawStartIndex;

            // *** On effectue la modif ***

            this.m_iDrawStartIndex++;
            this.CheckAndRefresh();

            // *** Vérification changement d'index ***

            if ( this.m_iDrawStartIndex != iOld && this.SelectedIndexChange != null )
                this.SelectedIndexChange(this, EventArgs.Empty);
        }

        /// <summary>
        /// Fait défilé sur la frame précédente
        /// </summary>
        public void FramePrevious()
        {
            this.m_iDrawStartIndex--;
            this.CheckAndRefresh();
        }

        /// <summary>
        /// Se positionne sur la première frame
        /// </summary>
        public void GoFirst()
        {
            this.m_iDrawStartIndex = 0;
            this.CheckAndRefresh();
        }

        /// <summary>
        /// Supprime une frame à l'animation
        /// </summary>
        /// <param name="p_iFrameIndex">Index de la place dans la liste</param>
        public void RemoveAt(int p_iListIndex)
        {
            if ( p_iListIndex < 0 || p_iListIndex >= this.m_oAnimation.Count )
                return;

            this.m_oAnimation.RemoveAt(p_iListIndex);
            this.CheckAndRefresh();

            // *** Modifié ***

            this.IsModified();
        }

        /// <summary>
        /// Vérifie si l'animation est en mode range, et le cas échéant si elle est valide.
        /// </summary>
        /// <returns>TRUE valide, FALSE en erreur</returns>
        public bool Verify(ref string p_sMessage)
        {
            // *** Vérification mode liste ***

            if ( !this.m_bRangeMode )
            {
                if ( this.m_oAnimation.Count < 1 )
                {
                    p_sMessage = "Vous devez sélectionner au moins une frame pour l'animation.";
                    return (false);
                }

                return (true);
            }

            // *** Vérification mode range ***

            if ( this.m_oAnimation.Count != 2 )
            {
                p_sMessage = "Vous devez sélectionner une frame de début et de fin.";
                return (false);
            }

            if ( this.m_oAnimation[0] >= this.m_oAnimation[1] )
            {
                p_sMessage = "La frame de début est supérieure ou égale à la frame de fin.";
                return (false);
            }

            // *** Terminée ***

            return (true);
        }

        #endregion

        #region Fonctions privées

        /// <summary>
        /// Vérifie les bornes et rafraichis
        /// </summary>
        private void CheckAndRefresh()
        {
            // *** Vérification bornes ***

            if ( this.m_iSelectedIndex < -1 )
                this.SelectedIndex = -1;
            else if ( this.m_iSelectedIndex >= this.m_oAnimation.Count )
                this.SelectedIndex = this.m_oAnimation.Count - 1;

            // *** Vérification bornes page ***

            if ( this.m_iDrawStartIndex < 0 )
                this.m_iDrawStartIndex = 0;
            else if ( this.m_oAnimation.Count > this.m_iDrawMaxPage && this.m_iDrawStartIndex > (this.m_oAnimation.Count - this.m_iDrawMaxPage) )
                this.m_iDrawStartIndex = this.m_oAnimation.Count - this.m_iDrawMaxPage;

            // *** Mise à jour des pages ***

            this.CheckPage();

            // *** On rafraichis l'affichage ***

            if ( this.ReDraw )
                this.Invalidate();
        }

        /// <summary>
        /// Déssine une frame dans le story board à un index X
        /// </summary>
        /// <param name="p_iIndex">Index dans la liste</param>
        /// <param name="p_iFrameIndex">Numéro de la frame</param>
        /// <param name="p_oFrame">Rectangle de la frame</param>
        /// <param name="p_iPosX">Position en X de départ</param>
        /// <param name="p_oGraphics">Instance du buffer de dessin</param>
        private void DrawImage(int p_iIndex, int p_iFrameIndex, Rectangle p_oFrame, int p_iPosX, Graphics p_oGraphics)
        {
            string sText;
            Rectangle oFinal;

            if ( p_oFrame.Width > this.m_iRefCadre || p_oFrame.Height > this.m_iRefCadre )
            {
                if ( p_oFrame.Width > p_oFrame.Height )
                {
                    oFinal = new Rectangle(0, 0, this.m_iRefCadre, Convert.ToInt32(Convert.ToSingle(this.m_iRefCadre) * (Convert.ToSingle(p_oFrame.Height) / Convert.ToSingle(p_oFrame.Width))));
                    oFinal.Y = (this.m_iRefCadre - oFinal.Height) / 2;
                }
                else
                {
                    oFinal = new Rectangle(0, 0, Convert.ToInt32(Convert.ToSingle(this.m_iRefCadre) * (Convert.ToSingle(p_oFrame.Width) / Convert.ToSingle(p_oFrame.Height))), this.m_iRefCadre);
                    oFinal.X = (this.m_iRefCadre - oFinal.Width) / 2;
                }
            }
            else
                oFinal = new Rectangle((this.m_iRefCadre / 2) - (p_oFrame.Width / 2), (this.m_iRefCadre / 2) - (p_oFrame.Height / 2), p_oFrame.Width, p_oFrame.Height);

            // *** Repositionnement de la zone finale ***

            oFinal.X += p_iPosX;
            oFinal.Y += 2;

            // *** On dessine la frame ***

            p_oGraphics.DrawImage(this.Texture, oFinal, p_oFrame, GraphicsUnit.Pixel);

            // *** On dessine le numéro de frame ***

            sText = string.Format("{0} [{1}]", p_iIndex, p_iFrameIndex + 1);

            p_oGraphics.DrawString(sText, this.m_oFont, this.m_oBrush, Convert.ToSingle((this.m_iRefCadre / 2) + p_iPosX) - (p_oGraphics.MeasureString(sText, this.m_oFont).Width / 2.0f), Convert.ToSingle(this.Height - this.m_oFont.Height - 3));
        }

        /// <summary>
        /// Dessine un story board en mode liste
        /// </summary>
        /// <param name="p_oGraphics">Instance du buffer de dessin</param>
        private void DrawList(Graphics p_oGraphics)
        {
            int iPosX = 2;

            for ( int i = this.m_iDrawStartIndex; i < (this.m_iDrawMaxPage + this.m_iDrawStartIndex) && i < this.m_oAnimation.Count; i++ )
            {
                this.DrawImage(i + 1, this.m_oAnimation[i], this.Frames[this.m_oAnimation[i]], iPosX, p_oGraphics);
                iPosX += this.m_iRefCadre + 5;
            }
        }

        /// <summary>
        /// Dessine un story board en mode range
        /// </summary>
        /// <param name="p_oGraphics">Instance du buffer de dessin</param>
        private void DrawRange(Graphics p_oGraphics)
        {
            // *** On déssine la première image ***

            if ( this.m_oAnimation.Count > 0 )
            {
                this.DrawImage(1, this.m_oAnimation[0], this.Frames[this.m_oAnimation[0]], 2, p_oGraphics);

                // *** On déssine la flèche ou le trait ***

                if ( this.m_oAnimation.Count > 1 )
                {
                    // *** On dessine la flèche ***

                    this.m_oPen.DashStyle = DashStyle.Solid;

                    p_oGraphics.DrawLine(this.m_oPen, this.m_iRefCadre + 14, (this.Height / 2) - Convert.ToInt32(this.m_oPen.Width) / 2, ((this.m_iRefCadre + 2) * 2) - 10, (this.Height / 2) - Convert.ToInt32(this.m_oPen.Width) / 2);

                    // *** On déssine la dernière ***

                    this.DrawImage(2, this.m_oAnimation[1], this.Frames[this.m_oAnimation[1]], ((this.m_iRefCadre + 2) * 2) + 2, p_oGraphics);
                }
                else
                {
                    // *** On dessine la ligne pointillé ***

                    this.m_oPen.DashStyle = DashStyle.Dash;

                    p_oGraphics.DrawLine(this.m_oPen, this.m_iRefCadre + 14, (this.Height / 2) - Convert.ToInt32(this.m_oPen.Width) / 2, ((this.m_iRefCadre + 2) * 2) - 10, (this.Height / 2) - Convert.ToInt32(this.m_oPen.Width) / 2);
                }
            }
        }

        /// <summary>
        /// Récupère la frame en cours de sélection sur un click
        /// </summary>
        /// <param name="p_iX">Coordonnée dans le contrôle en X</param>
        /// <returns>Index de l'élément, -1 sinon</returns>
        private int GetIndexOfSelection(int p_iX)
        {
            int iIndex;

            iIndex = p_iX / (this.m_iRefCadre + 4);

            // *** Vérification ***

            if ( iIndex >= this.m_iDrawMaxPage )
                return (-1);

            if ( this.m_bRangeMode && iIndex == 1 )
                return (-1);
            else if ( this.m_bRangeMode && iIndex == 2 && this.m_oAnimation.Count > 1 )
                return (1);

            if ( !this.m_bRangeMode && iIndex >= this.m_oAnimation.Count )
                return (-1);

            // *** On retourne l'index ***

            return (iIndex);
        }

        /// <summary>
        /// Met le contrôle en état modifié
        /// </summary>
        private void IsModified()
        {
            this.m_bToSave = true;

            if ( this.Modified != null )
                this.Modified(this, EventArgs.Empty);
        }

        #endregion

        #region Indexer

        /// <summary>
        /// Retourne la liste des animations
        /// </summary>
        public List<int> Animation
        {
            get
            {
                return (this.m_oAnimation);
            }
        }

        /// <summary>
        /// Retourne le nombre de frame utilisées dans l'animation
        /// </summary>
        [Browsable(false)]
        public int Count
        {
            get
            {
                return (this.m_oAnimation.Count);
            }
        }

        /// <summary>
        /// Affecte ou retourne les frames
        /// </summary>
        [Browsable(false)]
        public List<Rectangle> Frames
        {
            get;
            set;
        }

        /// <summary>
        /// Affecte ou retourne le mode de génération du story board
        /// </summary>
        [Browsable(true), DefaultValue(false), Description("Définit le mode de génération du story board"), Category("Custom")]
        public bool RangeMode
        {
            get
            {
                return (this.m_bRangeMode);
            }
            set
            {
                this.m_bRangeMode = value;

                // *** Si mode range et plus de 2 frames on tronque ***

                if ( this.m_bRangeMode && this.m_oAnimation.Count > 2 )
                {
                    for ( int i = 2; i < this.m_oAnimation.Count; i++ )
                    {
                        this.m_oAnimation.RemoveAt(i);
                        i--;
                    }
                }

                // *** Vérification des zones ***

                this.CheckAndRefresh();

                // *** Modifié ***

                this.IsModified();
            }
        }

        /// <summary>
        /// Affecte ou retourne si le contrôle se redessine automatiquement
        /// </summary>
        [Browsable(true), DefaultValue(true), Description("Définit si le contrôle se redessine automatiquement"), Category("Custom")]
        public bool ReDraw
        {
            get
            {
                return (this.m_bReDraw);
            }
            set
            {
                this.m_bReDraw = value;

                if ( this.m_bReDraw )
                    this.Invalidate();
            }
        }

        /// <summary>
        /// Affecte ou retourne la sélection actuelle de l'index
        /// </summary>
        [Browsable(false)]
        public int SelectedIndex
        {
            get
            {
                return (this.m_iSelectedIndex + this.m_iDrawStartIndex);
            }
            set
            {
                // *** Vérification ***

                if ( value < -1 || value >= this.m_oAnimation.Count )
                    return;

                // *** On sauvegarde l'ancien statut pour l'évent ***

                int iOld = this.m_iSelectedIndex;

                // *** Mise à jour ***

                this.m_iSelectedIndex = value - this.m_iDrawStartIndex;
                this.CheckAndRefresh();

                // *** Raise event si changement ***

                if ( iOld != this.m_iSelectedIndex && this.SelectedIndexChange != null )
                    this.SelectedIndexChange(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Affecte ou retourne la texture source
        /// </summary>
        [Browsable(false)]
        public Image Texture
        {
            get;
            set;
        }

        /// <summary>
        /// Affecte ou retourne si le contrôle à été modifié
        /// </summary>
        public bool ToSave
        {
            get
            {
                return (this.m_bToSave);
            }
            set
            {
                this.m_bToSave = value;
            }
        }

        #endregion
    }
}
